<?php
/*
 * Copyright (c) 2010, webvariants GbR, http://www.webvariants.de
 *
 * Diese Datei steht unter der MIT-Lizenz. Der Lizenztext befindet sich in der
 * beiliegenden LICENSE Datei und unter:
 *
 * http://www.opensource.org/licenses/mit-license.php
 * http://de.wikipedia.org/wiki/MIT-Lizenz
 */

/**
 * Eine virtuelle Route
 *
 * Eine Route ist eine Möglichkeit für AddOns, selber schöne URLs über realURL2
 * generieren zu lassen. Dazu reicht es, ein WV24_Route-Objekt zu erzeugen,
 * Parameter zu binden und dann getURL() aufzurufen.
 *
 * Routen sind immer relativ zu der Kategorie des Artikels, dessen Code die
 * Route anlegt. Man muss die Kategorie also nicht manuell in die Route
 * reinprügeln. Wenn das Verhalten nicht gewünscht ist, kann eine Route
 * absolut zum Projekt-Root gesetzt werden, indem sie mit einem Slash beginnt.
 *
 * Jeder Route stehen eine Reihe von vordefinierten Platzhaltern zur Verfügung,
 * die automatisch mit den passenden Werten belegt werden.
 *
 * - <code>rexartid</code> (ID des Ziel-Artikels)
 * - <code>rexcatid</code> (Kategorie-ID des Ziel-Artikels)
 * - <code>clangid</code> (Sprach-ID des Ziel-Artikels)
 * - <code>clang</code> (Alias für clangid)
 *
 * Außerdem wird zu jeder URL automatisch die Kategorie-ID in
 * <code>category_id</code> wiederhergestellt.
 *
 * Zu jeder URL können neben den Werten für die eigentliche URL (z. B.
 * Produktnamen in einem Shop) auch noch beliebige zusätzliche Werte gebunden
 * werden, die in der Datenbank zu der URL abgelegt und bei einem Aufruf
 * wiederhergestellt werden. Damit kann zu der URL "foo/bar/produkt1.html"
 * die Produkt-ID mitgespeichert werden, um auf sie direkt im entsprechenden
 * Modul zugreifen zu können.
 *
 * Diese Implementierung ist noch ein Entwurf und noch nicht funktionstüchtig.
 *
 * @since  v0.8.5
 * @author Christoph
 */
class WV24_Route
{
	protected $route;      ///< string  die Route als String
	protected $params;     ///< array   die gebundenen Parameter
	protected $articleID;  ///< int     die ID des Ziel-Artikels
	protected $clang;      ///< int     die Sprach-ID des Ziel-Artikels
	
	/**
	 * Konstruktor
	 *
	 * Erzeugt eine neue Route. Eine Route ist dabei nur eine Bildungsvorschrift
	 * für neue URLs. Routen selbst werden nicht permanent gespeichert, dafür
	 * aber die nach ihrem Muster erzeugten URLs.
	 *
	 * In die Route können Platzhalter eingebettet werden. Platzhalter beginnen
	 * mit einem Doppelpunkt und nehmen dann alle folgenden alphanumerischen
	 * Zeichen ein. Beispiel: "hallo/:kat/:prod.html" enthält die beiden
	 * Platzhalter kat und prod, die später mit bind() mit Werten belegt
	 * werden.
	 *
	 * @throws Exception      falls die Route leer ist
	 * @param  string $route  die URL (meist mit Platzhaltern)
	 */
	public function __construct($route)
	{
		$this->setRoute($route);
		
		$this->params    = array();
		$this->articleID = WV_Redaxo::getCurrentArticleID();
		$this->clang     = WV_Redaxo::clang();
	}
	
	/**
	 * Route setzen
	 *
	 * Überschreibt das interne "Muster", nach dem die URLs erzeugt werden.
	 *
	 * @throws Exception      falls die Route leer ist
	 * @param  string $route  die neue URL (meist mit Platzhaltern)
	 */
	public function setRoute($route)
	{
		$route = trim(strtolower($route));
		
		if (empty($route)) {
			throw new Exception('Leere Routen sind nicht erlaubt.');
		}
		
		$this->route = $route;
	}
	
	/**
	 * Wert binden
	 *
	 * Diese Methode füllt eine Route mit entsprechenden Werten. Dabei wird in
	 * $param der Doppelpunkt des Platzhalters nicht mit angegeben. Wird $restore
	 * auf true gesetzt, wird der gebundene Wert in der Datenbank zusätzlich
	 * gespeichert und bei einem Request wiederhergestellt.
	 *
	 * Es gibt keine Beschränkung, welche Werte an eine URL gebunden werden
	 * können. Es werden allerdings nur diejenigen verwendet, deren Platzhalter
	 * in der Route auftauchen oder die bei einem Request wiederhergestellt
	 * werden sollen. Alle weiteren hängen unmotiviert bis zum Scriptende im
	 * Speicher rum.
	 *
	 * @param  string  $param    der Name des Platzhalters
	 * @param  mixed   $value    der entsprechende Wert (keine Ressourcen oder File Handles!)
	 * @param  boolean $restore  wenn true, wird der Wert in $_GET bei einem Request wiederhergestellt
	 * @return int               die Anzahl der nun gebundenen Parameter
	 */
	public function bind($param, $value, $restore = false)
	{
		$this->params[$param] = array('value' => $value, 'restore' => (boolean) $restore);
		return count($this->params);
	}
	
	/**
	 * Mehrere Werte auf einmal binden
	 *
	 * Diese Methode füllt eine Route mit entsprechenden Werten. Dabei wird in
	 * $params ein assoziatives Array mit den Werten angegeben. Wird $restore
	 * auf true gesetzt, werden alle gebundenen Werte bei einem Request
	 * wiederhergestellt.
	 *
	 * Siehe bind() für die Beschränkungen, welche Parameter erlaubt sind.
	 *
	 * @param  array   $params   die zu bindenden Werte als assoziatives Array
	 * @param  boolean $restore  wenn true, werden alle Werte in $_GET bei einem Request wiederhergestellt
	 * @return int               die Anzahl der nun gebundenen Parameter
	 */
	public function bindMulti($params, $restore = false)
	{
		$restore = (boolean) $restore;
		
		foreach ($params as $key => $value) {
			$this->params[$key] = array('value' => $value, 'restore' => $restore);
		}
		
		return count($this->params);
	}
	
	/**
	 * Entfernt einen Wert wieder
	 *
	 * Diese Methode entfernt einen gesetzten Wert wieder aus der Liste.
	 *
	 * @param  string $param  der Name des Platzhalters
	 * @return int            die Anzahl der nun gebundenen Parameter
	 */
	public function unbind($param)
	{
		unset($this->params[$param]);
		return count($this->params);
	}
	
	/**
	 * Alle Werte entfernen
	 *
	 * Diese Methode entfernt alle gesetzten Werte.
	 *
	 * @return int  immer 0
	 */
	public function unbindAll()
	{
		$this->params = array();
		return 0;
	}
	
	/**
	 * Ziel-Artikel setzen
	 *
	 * Diese Methode setzt den Artikel, auf den die erzeugten URLs zeigen sollen.
	 *
	 * @param  mixed  $article  null für den aktuellen Artikel, sonst irgend etwas, das als Artikel identifiziert werden kann
	 * @return int              die ermittelte Artikel-ID
	 */
	public function setArticle($article)
	{
		$this->articleID = $article === null ? WV_Redaxo::getCurrentArticleID() : WV_Redaxo::getArticleID($article);
		return $this->articleID;
	}
	
	/**
	 * Ziel-Sprache setzen
	 *
	 * Diese Methode setzt die Sprache, auf die die erzeugten URLs zeigen sollen.
	 *
	 * @param  int $clang  die gewünschte Sprache
	 * @return int         die ermittelte Sprach-ID
	 */
	public function setCLang($clang)
	{
		$this->clang = WV_Redaxo::clang($clang);
		return $this->clang;
	}
	
	/**
	 * URL erzeugen
	 *
	 * Diese Methode erzeugt schlussendlich die fertige URL zu der aktuellen
	 * Route. Die URL wird automatisch im Cache abgelegt.
	 *
	 * $article und $clang können ausgelassen werden, wenn sie vorher mit
	 * den entsprechenden Settern gesetzt wurden.
	 *
	 * @param  mixed   $article      false, um den vorher via setArticle() gesetzten
	 *                               Artikel zu verwenden, sonst etwas, was setArticle()
	 *                               versteht.
	 * @param  mixed   $clang        false, um die vorher via setCLang() gesetzte
	 *                               Sprache zu verwenden, sonst etwas, was setCLang()
	 *                               versteht.
	 * @param  mixed   $params       die GET-Parameter entweder als Array oder bereits als Query-String
	 * @param  string  $divider      der Divider für den Query-String (nur relevant, wenn $params ein array ist)
	 * @param  boolean $forceDomain  wenn true, wird immer die Domain eingefügt (ansonsten nur wenn nötig)
	 * @return string                die fertige URL
	 */
	public function getURL($article = false, $clang = false, $params = array(), $divider = '&amp;', $forceDomain = false)
	{
		$isAbsolute = $this->route[0] == '/';
		$articleID  = $article === false ? $this->articleID : $this->setArticle($article);
		$clang      = $clang === false ? $this->clang : $this->setCLang($clang);
		$category   = $isAbsolute ? OOArticle::getSiteStartArticle($clang) : OOArticle::getArticleById($articleID, $clang)->getCategory();
		$categoryID = $category->getId();
		
		// Immer verfügbare Variablen setzen
		
		$this->bindCoreVariables($articleID, $clang, $categoryID);
		
		// URL zur Kategorie / Startseite (für absolute Routen) generieren
		
		$categoryURL  = WV24_RealURL2::getURL($categoryID, $clang, $params, $divider, true);
		$trimmedRoute = ltrim($this->route, '/');
		
		// URL in Pfad und Query-String aufsplitten
		
		$parts       = explode('?', $categoryURL, 2);
		$path        = $parts[0];
		$queryString = isset($parts[1]) ? $parts[1] : '';
		
		// Platzhalter in der Route ersetzen
		
		preg_match_all('#(:[a-z][a-z0-9]+)#si', $trimmedRoute, $matches);
		
		if (!empty($matches)) {
			foreach ($matches[1] as $match) {
				$param    = substr($match, 1);
				$value    = isset($this->params[$param]) ? $this->params[$param]['value'] : '';
				$friendly = WV24_RealURL2::makeURLFriendly($value, '');
				
				$trimmedRoute = str_replace($match, $friendly, $trimmedRoute);
			}
		}
		
		// Route an die Standard-URL anfügen
		
		$url = $path.$trimmedRoute;
		
		// Query-String
		
		if (!empty($queryString)) {
			$url .= '?'.$queryString;
		}
		
		// URL und Parameter cachen
		
		$cache = array();
		
		foreach ($this->params as $key => $data) {
			if ($data['restore']) {
				$cache[$key] = $data['value'];
			}
		}
		
		$hash = WV24_RealURL2::hashURL($url);
		$sql  = WV_SQLEx::getInstance();
		$path = $isAbsolute ? '|' : $category->getValue('path');
		
		$sql->queryEx(
			'REPLACE INTO #_wv24_route_cache (article_id, clang, path, url, hash, created, params) VALUES (?,?,?,?,?,NOW(),?)',
			array($articleID, $clang, $path, $url, $hash, json_encode($cache)), '#_'
		);
		
		// Fertig!
		
		if (!$forceDomain) {
			$url = WV24_RealURL2::removeDomainIfPossible($url);
		}
		
		return $url;
	}
	
	/**
	 * Core-Variablen setzen
	 *
	 * Jeder Route stehen automatisch die in der Klassenbeschreibung genannten
	 * Platzhalter zur Verfügung. Diese werden in dieser Methode mit sinnvollen
	 * Werten belegt.
	 *
	 * Da diese Methode von getURL() aufgerufen wird, ist es nicht möglich,
	 * die Core-Variablen mit eigenen Werten zu überschreiben.
	 *
	 * @param int $articleID   die ID des Ziel-Artikels
	 * @param int $clang       die Sprach-ID des Ziel-Artikels
	 * @param int $categoryID  die Kategorie-ID des Ziel-Artikels
	 */
	protected function bindCoreVariables($articleID, $clang, $categoryID)
	{
		global $REX;
		
		$this->bind(':rexartid', $articleID, false);
		$this->bind(':rexcatid', $categoryID, false);
		$this->bind(':clangid', $clang, false);
		$this->bind(':clang', $REX['CLANG'][$clang], false);
		
		// wiederherzustellende GET-Parameter (article_id und clang werden unabhängig gespeichert)
		
		$this->bind('category_id', $categoryID, true);
	}
}
